@objectstack/runtime 6.5.0 → 6.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -2141,6 +2141,7 @@ __export(index_exports, {
2141
2141
  resolveDefaultArtifactPath: () => resolveDefaultArtifactPath,
2142
2142
  resolveErrorReporter: () => resolveErrorReporter,
2143
2143
  resolveMetrics: () => resolveMetrics,
2144
+ resolveObjectStackHome: () => resolveObjectStackHome,
2144
2145
  resolveRequestId: () => resolveRequestId,
2145
2146
  seedPlatformSsoClient: () => seedPlatformSsoClient
2146
2147
  });
@@ -2196,8 +2197,17 @@ var Runtime = class {
2196
2197
  // src/standalone-stack.ts
2197
2198
  var import_node_path2 = require("path");
2198
2199
  var import_node_fs = require("fs");
2200
+ var import_node_os = require("os");
2199
2201
  var import_zod = require("zod");
2200
2202
  init_load_artifact_bundle();
2203
+ function resolveObjectStackHome() {
2204
+ const raw = process.env.OS_HOME?.trim();
2205
+ if (raw && raw.length > 0) {
2206
+ if (raw.startsWith("~")) return (0, import_node_path2.resolve)((0, import_node_os.homedir)(), raw.slice(1).replace(/^[/\\]/, ""));
2207
+ return (0, import_node_path2.resolve)(raw);
2208
+ }
2209
+ return (0, import_node_path2.resolve)((0, import_node_os.homedir)(), ".objectstack");
2210
+ }
2201
2211
  var StandaloneStackConfigSchema = import_zod.z.object({
2202
2212
  databaseUrl: import_zod.z.string().optional(),
2203
2213
  databaseAuthToken: import_zod.z.string().optional(),
@@ -2228,7 +2238,7 @@ async function createStandaloneStack(config) {
2228
2238
  const environmentId = cfg.environmentId ?? process.env.OS_ENVIRONMENT_ID ?? "proj_local";
2229
2239
  const artifactPathInput = cfg.artifactPath ?? process.env.OS_ARTIFACT_PATH ?? (0, import_node_path2.resolve)(cwd, "dist/objectstack.json");
2230
2240
  const artifactPath = isHttpUrl(artifactPathInput) ? artifactPathInput : artifactPathInput.startsWith("/") ? artifactPathInput : (0, import_node_path2.resolve)(cwd, artifactPathInput);
2231
- const dbUrl = cfg.databaseUrl ?? process.env.OS_DATABASE_URL?.trim() ?? process.env.TURSO_DATABASE_URL?.trim() ?? `file:${(0, import_node_path2.resolve)(cwd, ".objectstack/data/standalone.db")}`;
2241
+ const dbUrl = cfg.databaseUrl ?? process.env.OS_DATABASE_URL?.trim() ?? process.env.TURSO_DATABASE_URL?.trim() ?? `file:${(0, import_node_path2.resolve)(resolveObjectStackHome(), "data/standalone.db")}`;
2232
2242
  const dbAuthToken = cfg.databaseAuthToken ?? process.env.OS_DATABASE_AUTH_TOKEN?.trim() ?? process.env.TURSO_AUTH_TOKEN?.trim();
2233
2243
  const explicitDriver = cfg.databaseDriver ?? process.env.OS_DATABASE_DRIVER?.trim();
2234
2244
  const dbDriver = explicitDriver ?? detectDriverFromUrl(dbUrl);
@@ -2348,12 +2358,42 @@ function resolveDefaultArtifactPath(explicitPath, cwd = process.cwd()) {
2348
2358
  }
2349
2359
  async function createDefaultHostConfig(options = {}) {
2350
2360
  const { requireArtifact = true, ...standaloneOpts } = options;
2351
- const resolvedArtifact = resolveDefaultArtifactPath(standaloneOpts.artifactPath);
2361
+ let resolvedArtifact = resolveDefaultArtifactPath(standaloneOpts.artifactPath);
2352
2362
  if (!resolvedArtifact && requireArtifact) {
2353
2363
  throw new Error(
2354
2364
  "[createDefaultHostConfig] No artifact source available. Set OS_ARTIFACT_PATH (file path or http(s):// URL), place the artifact at <cwd>/dist/objectstack.json, or pass `{ artifactPath: ... }` explicitly. To boot an empty kernel anyway, pass `{ requireArtifact: false }`."
2355
2365
  );
2356
2366
  }
2367
+ if (!resolvedArtifact && !requireArtifact) {
2368
+ const home = resolveObjectStackHome();
2369
+ const stubPath = (0, import_node_path3.resolve)(home, "dist/objectstack.json");
2370
+ if (!(0, import_node_fs2.existsSync)(stubPath)) {
2371
+ (0, import_node_fs2.mkdirSync)((0, import_node_path3.resolve)(stubPath, ".."), { recursive: true });
2372
+ (0, import_node_fs2.writeFileSync)(
2373
+ stubPath,
2374
+ JSON.stringify(
2375
+ {
2376
+ manifest: {
2377
+ id: "com.objectstack.empty",
2378
+ name: "empty",
2379
+ version: "0.0.0",
2380
+ type: "app",
2381
+ description: "Empty starter kernel \u2014 install apps via the Studio marketplace."
2382
+ },
2383
+ objects: [],
2384
+ views: [],
2385
+ apps: [],
2386
+ flows: [],
2387
+ requires: []
2388
+ },
2389
+ null,
2390
+ 2
2391
+ ),
2392
+ "utf8"
2393
+ );
2394
+ }
2395
+ resolvedArtifact = stubPath;
2396
+ }
2357
2397
  return createStandaloneStack({
2358
2398
  ...standaloneOpts,
2359
2399
  artifactPath: resolvedArtifact
@@ -6901,6 +6941,8 @@ var KernelManager = class {
6901
6941
  this.maxSize = config.maxSize ?? 32;
6902
6942
  this.ttlMs = config.ttlMs ?? 15 * 60 * 1e3;
6903
6943
  this.logger = config.logger ?? console;
6944
+ this.freshnessProbe = config.freshnessProbe;
6945
+ this.staleCheckIntervalMs = config.staleCheckIntervalMs ?? 1e4;
6904
6946
  }
6905
6947
  /** Returns the currently cached environmentIds (ordered by insertion). */
6906
6948
  keys() {
@@ -6923,8 +6965,31 @@ var KernelManager = class {
6923
6965
  if (this.ttlMs > 0 && Date.now() - existing.lastAccess > this.ttlMs) {
6924
6966
  await this.evict(environmentId);
6925
6967
  } else {
6926
- existing.lastAccess = Date.now();
6927
- return existing.kernel;
6968
+ if (this.freshnessProbe) {
6969
+ const now = Date.now();
6970
+ if (now - existing.lastStaleCheckAt >= this.staleCheckIntervalMs) {
6971
+ existing.lastStaleCheckAt = now;
6972
+ let stale = false;
6973
+ try {
6974
+ stale = await this.freshnessProbe(environmentId, existing.createdAt);
6975
+ } catch (err) {
6976
+ this.logger.warn?.("[KernelManager] freshness probe failed", { environmentId, err });
6977
+ }
6978
+ if (stale) {
6979
+ this.logger.info?.("[KernelManager] kernel evicted by freshness probe", { environmentId });
6980
+ await this.evict(environmentId);
6981
+ } else {
6982
+ existing.lastAccess = Date.now();
6983
+ return existing.kernel;
6984
+ }
6985
+ } else {
6986
+ existing.lastAccess = Date.now();
6987
+ return existing.kernel;
6988
+ }
6989
+ } else {
6990
+ existing.lastAccess = Date.now();
6991
+ return existing.kernel;
6992
+ }
6928
6993
  }
6929
6994
  }
6930
6995
  const inflight = this.pending.get(environmentId);
@@ -6932,7 +6997,7 @@ var KernelManager = class {
6932
6997
  const promise = (async () => {
6933
6998
  const kernel = await this.factory.create(environmentId);
6934
6999
  const now = Date.now();
6935
- this.cache.set(environmentId, { kernel, createdAt: now, lastAccess: now });
7000
+ this.cache.set(environmentId, { kernel, createdAt: now, lastAccess: now, lastStaleCheckAt: now });
6936
7001
  await this.enforceMaxSize();
6937
7002
  return kernel;
6938
7003
  })();
@@ -7100,6 +7165,30 @@ var ArtifactApiClient = class {
7100
7165
  if (!found?.headCommitId) return null;
7101
7166
  return { commitId: String(found.headCommitId), publishedAt: found.headPublishedAt ?? null };
7102
7167
  }
7168
+ /**
7169
+ * Cheap freshness probe — returns the env's `last_published_at`
7170
+ * (and best-effort current commit) without rebuilding the artifact.
7171
+ * Used by `KernelManager` on cache hits to detect when a per-env
7172
+ * kernel has been invalidated by an upstream change (marketplace
7173
+ * install/uninstall, artifact publish) so it can be rebuilt
7174
+ * without waiting for the 15-minute LRU TTL to expire.
7175
+ *
7176
+ * Returns `null` on definitive 404 / unknown env. Errors propagate
7177
+ * (caller decides whether to treat unreachable cloud as fresh or
7178
+ * stale — typically fresh, so a brief outage doesn't churn every
7179
+ * cached kernel).
7180
+ */
7181
+ async getFreshness(environmentId) {
7182
+ const url = `${this.base}/api/v1/cloud/environments/${encodeURIComponent(environmentId)}/freshness`;
7183
+ const res = await this.request(url);
7184
+ if (res === null) return null;
7185
+ const body = res.success === false ? null : res.data ?? res;
7186
+ if (!body || typeof body !== "object") return null;
7187
+ const envId = typeof body.environmentId === "string" ? body.environmentId : environmentId;
7188
+ const lastPublishedAt = typeof body.lastPublishedAt === "string" ? body.lastPublishedAt : null;
7189
+ const commitId = typeof body.commitId === "string" ? body.commitId : null;
7190
+ return { environmentId: envId, lastPublishedAt, commitId };
7191
+ }
7103
7192
  /** Drop cached entries for a project (and any matching hostname). */
7104
7193
  invalidate(environmentId) {
7105
7194
  this.artifactCache.delete(environmentId);
@@ -7851,7 +7940,30 @@ var ArtifactKernelFactory = class {
7851
7940
  };
7852
7941
 
7853
7942
  // src/cloud/auth-proxy-plugin.ts
7943
+ var import_node_crypto3 = require("crypto");
7854
7944
  var AUTH_PREFIX = "/api/v1/auth";
7945
+ function signSessionCookieValue(rawToken, secret) {
7946
+ const signature = (0, import_node_crypto3.createHmac)("sha256", secret).update(rawToken).digest("base64");
7947
+ return encodeURIComponent(`${rawToken}.${signature}`);
7948
+ }
7949
+ function buildSetCookieHeader(name, encodedValue, attrs, maxAgeSec) {
7950
+ const parts = [`${name}=${encodedValue}`];
7951
+ const a = attrs ?? {};
7952
+ if (a.path) parts.push(`Path=${a.path}`);
7953
+ else parts.push("Path=/");
7954
+ if (Number.isFinite(maxAgeSec) && maxAgeSec > 0) parts.push(`Max-Age=${Math.floor(maxAgeSec)}`);
7955
+ if (a.domain) parts.push(`Domain=${a.domain}`);
7956
+ if (a.sameSite) {
7957
+ const ss = String(a.sameSite);
7958
+ parts.push(`SameSite=${ss.charAt(0).toUpperCase() + ss.slice(1)}`);
7959
+ } else {
7960
+ parts.push("SameSite=Lax");
7961
+ }
7962
+ if (a.secure) parts.push("Secure");
7963
+ if (a.httpOnly !== false) parts.push("HttpOnly");
7964
+ if (a.partitioned) parts.push("Partitioned");
7965
+ return parts.join("; ");
7966
+ }
7855
7967
  function pickHandler(svc) {
7856
7968
  if (!svc) return void 0;
7857
7969
  if (typeof svc.handleRequest === "function") return svc.handleRequest.bind(svc);
@@ -7945,6 +8057,115 @@ var AuthProxyPlugin = class {
7945
8057
  return c.json({ hasOwner: true });
7946
8058
  }
7947
8059
  }
8060
+ if (c.req.method === "POST" && subPath === "sso-handoff-issue") {
8061
+ try {
8062
+ const expected = (process.env.OS_CLOUD_API_KEY ?? "").trim();
8063
+ if (!expected) {
8064
+ return c.json({ error: "sso_handoff_disabled", reason: "OS_CLOUD_API_KEY unset on env runtime" }, 503);
8065
+ }
8066
+ const authz = c.req.header("authorization") ?? "";
8067
+ const provided = authz.toLowerCase().startsWith("bearer ") ? authz.slice(7).trim() : "";
8068
+ if (!provided || provided !== expected) {
8069
+ return c.json({ error: "unauthorized" }, 401);
8070
+ }
8071
+ if (typeof authSvc?.getAuthContext !== "function") {
8072
+ return c.json({ error: "auth_service_unavailable" }, 503);
8073
+ }
8074
+ const handoffAuthCtx = await authSvc.getAuthContext();
8075
+ const internal = handoffAuthCtx?.internalAdapter;
8076
+ if (!internal?.createVerificationValue) {
8077
+ return c.json({ error: "verification_api_unavailable" }, 503);
8078
+ }
8079
+ let body = {};
8080
+ try {
8081
+ body = await c.req.json();
8082
+ } catch {
8083
+ body = {};
8084
+ }
8085
+ const email = String(body?.email ?? "").toLowerCase().trim();
8086
+ if (!email) return c.json({ error: "email_required" }, 400);
8087
+ const name = body?.name == null ? null : String(body.name);
8088
+ const by = body?.by == null ? "service" : String(body.by);
8089
+ const envIdInBody = body?.envId == null ? null : String(body.envId);
8090
+ const handoff = (0, import_node_crypto3.randomUUID)().replace(/-/g, "") + (0, import_node_crypto3.randomUUID)().replace(/-/g, "");
8091
+ const ttlSec = 60;
8092
+ const expiresAt = new Date(Date.now() + ttlSec * 1e3);
8093
+ await internal.createVerificationValue({
8094
+ identifier: `sso-handoff:${handoff}`,
8095
+ value: JSON.stringify({ email, name, by, envId: envIdInBody ?? environmentId }),
8096
+ expiresAt
8097
+ });
8098
+ return c.json({
8099
+ token: handoff,
8100
+ expiresAt: expiresAt.toISOString(),
8101
+ ttlSec
8102
+ });
8103
+ } catch (err) {
8104
+ ctx.logger?.error?.("[AuthProxyPlugin] sso-handoff-issue failed", err instanceof Error ? err : new Error(String(err)));
8105
+ return c.json({ error: "sso_handoff_issue_failed", message: String(err?.message ?? err) }, 500);
8106
+ }
8107
+ }
8108
+ if (c.req.method === "GET" && subPath === "sso-exchange") {
8109
+ try {
8110
+ const token = (url.searchParams.get("token") ?? "").trim();
8111
+ const nextRaw = url.searchParams.get("next") ?? "/";
8112
+ const next = nextRaw.startsWith("/") ? nextRaw : "/";
8113
+ if (!token) return c.text("missing token", 400);
8114
+ if (typeof authSvc?.getAuthContext !== "function") {
8115
+ return c.text("auth service unavailable", 503);
8116
+ }
8117
+ const authCtx = await authSvc.getAuthContext();
8118
+ const internal = authCtx?.internalAdapter;
8119
+ if (!internal?.consumeVerificationValue) {
8120
+ return c.text("verification API unavailable", 503);
8121
+ }
8122
+ const consumed = await internal.consumeVerificationValue(`sso-handoff:${token}`);
8123
+ if (!consumed) return c.text("invalid or expired token", 401);
8124
+ const expiresAt = consumed?.expiresAt ? new Date(consumed.expiresAt).getTime() : 0;
8125
+ if (!expiresAt || expiresAt < Date.now()) return c.text("expired token", 401);
8126
+ let payload = {};
8127
+ try {
8128
+ payload = JSON.parse(String(consumed.value));
8129
+ } catch {
8130
+ payload = { email: String(consumed.value) };
8131
+ }
8132
+ const email = String(payload.email ?? "").toLowerCase().trim();
8133
+ if (!email) return c.text("handoff missing email", 400);
8134
+ const found = await internal.findUserByEmail(email, { includeAccounts: true });
8135
+ let userId = found?.user?.id;
8136
+ let hasCredentialAccount = (found?.accounts ?? []).some((a) => a.providerId === "credential" && a.password);
8137
+ if (!userId) {
8138
+ const created = await internal.createUser({
8139
+ email,
8140
+ name: payload.name ?? email,
8141
+ emailVerified: true
8142
+ });
8143
+ userId = created?.id;
8144
+ hasCredentialAccount = false;
8145
+ }
8146
+ if (!userId) return c.text("failed to provision user", 500);
8147
+ const session = await internal.createSession(userId, false);
8148
+ const rawToken = session?.token;
8149
+ const sessionExpiresAt = session?.expiresAt ? new Date(session.expiresAt) : new Date(Date.now() + 7 * 24 * 3600 * 1e3);
8150
+ if (!rawToken) return c.text("failed to mint session", 500);
8151
+ const secret = authCtx?.secret ?? "";
8152
+ if (!secret) return c.text("auth secret unavailable", 503);
8153
+ const cookieName = authCtx?.authCookies?.sessionToken?.name ?? "better-auth.session_token";
8154
+ const cookieAttrs = authCtx?.authCookies?.sessionToken?.attributes ?? {};
8155
+ const encoded = signSessionCookieValue(rawToken, secret);
8156
+ const maxAgeSec = Math.max(60, Math.floor((sessionExpiresAt.getTime() - Date.now()) / 1e3));
8157
+ const setCookie = buildSetCookieHeader(cookieName, encoded, cookieAttrs, maxAgeSec);
8158
+ const finalNext = hasCredentialAccount ? next : `/_console/system/profile?recovery_needed=true&next=${encodeURIComponent(next)}`;
8159
+ const headers = new Headers();
8160
+ headers.set("Set-Cookie", setCookie);
8161
+ headers.set("Location", finalNext);
8162
+ headers.set("Cache-Control", "no-store");
8163
+ return new Response(null, { status: 302, headers });
8164
+ } catch (err) {
8165
+ ctx.logger?.error?.("[AuthProxyPlugin] sso-exchange failed", err instanceof Error ? err : new Error(String(err)));
8166
+ return c.text(`sso-exchange failed: ${err?.message ?? String(err)}`, 500);
8167
+ }
8168
+ }
7948
8169
  const fn = await resolveAuthHandler(authSvc);
7949
8170
  if (!fn) {
7950
8171
  return c.json({ error: "auth_service_unavailable", environmentId }, 503);
@@ -8127,20 +8348,46 @@ var RuntimeConfigPlugin = class {
8127
8348
  return;
8128
8349
  }
8129
8350
  const rawApp = httpServer.getRawApp();
8130
- const payload = {
8131
- cloudUrl: this.cloudUrl,
8132
- singleEnvironment: this.singleEnvironment,
8133
- features: {
8134
- installLocal: this.installLocal,
8135
- marketplace: true
8351
+ const features = {
8352
+ installLocal: this.installLocal,
8353
+ marketplace: true
8354
+ };
8355
+ let envRegistry = null;
8356
+ try {
8357
+ envRegistry = ctx.getService("env-registry");
8358
+ } catch {
8359
+ }
8360
+ const handler = async (c) => {
8361
+ const rawHost = c.req.header("host") ?? "";
8362
+ const host = rawHost.split(":")[0].toLowerCase().trim();
8363
+ let defaultEnvironmentId;
8364
+ let defaultOrgId;
8365
+ let resolvedSingleEnv = this.singleEnvironment;
8366
+ if (envRegistry && host && typeof envRegistry.resolveHostname === "function") {
8367
+ try {
8368
+ const resolved = await envRegistry.resolveHostname(host);
8369
+ if (resolved?.environmentId) {
8370
+ defaultEnvironmentId = resolved.environmentId;
8371
+ if (resolved.organizationId) defaultOrgId = String(resolved.organizationId);
8372
+ resolvedSingleEnv = true;
8373
+ }
8374
+ } catch {
8375
+ }
8136
8376
  }
8377
+ return c.json({
8378
+ cloudUrl: this.cloudUrl,
8379
+ singleEnvironment: resolvedSingleEnv,
8380
+ defaultOrgId,
8381
+ defaultEnvironmentId,
8382
+ features
8383
+ });
8137
8384
  };
8138
- const handler = (c) => c.json(payload);
8139
8385
  rawApp.get("/api/v1/runtime/config", handler);
8140
8386
  rawApp.get("/api/v1/studio/runtime-config", handler);
8141
8387
  ctx.logger?.info?.("[RuntimeConfigPlugin] mounted /api/v1/runtime/config", {
8142
8388
  cloudUrl: this.cloudUrl || "(empty)",
8143
- installLocal: this.installLocal
8389
+ installLocal: this.installLocal,
8390
+ perHostEnvResolution: !!envRegistry
8144
8391
  });
8145
8392
  });
8146
8393
  };
@@ -8341,7 +8588,21 @@ var ObjectOSEnvironmentPlugin = class {
8341
8588
  factory,
8342
8589
  maxSize: this.config.kernelCacheSize,
8343
8590
  ttlMs: this.config.kernelTtlMs,
8344
- logger: ctx.logger
8591
+ logger: ctx.logger,
8592
+ // Only the HTTP client exposes /freshness; file-mode (CLI dev)
8593
+ // has no upstream to probe.
8594
+ freshnessProbe: this.config.controlPlaneUrl === "file" ? void 0 : async (envId, builtAtMs) => {
8595
+ const fresh = await client.getFreshness(envId);
8596
+ if (!fresh) return false;
8597
+ const t = fresh.lastPublishedAt ? Date.parse(fresh.lastPublishedAt) : NaN;
8598
+ if (!Number.isFinite(t)) return false;
8599
+ if (t <= builtAtMs) return false;
8600
+ try {
8601
+ client.invalidate(envId);
8602
+ } catch {
8603
+ }
8604
+ return true;
8605
+ }
8345
8606
  });
8346
8607
  this.kernelManager = kernelManager;
8347
8608
  ctx.registerService("env-registry", envRegistry);
@@ -8942,6 +9203,7 @@ __reExport(index_exports, require("@objectstack/core"), module.exports);
8942
9203
  resolveDefaultArtifactPath,
8943
9204
  resolveErrorReporter,
8944
9205
  resolveMetrics,
9206
+ resolveObjectStackHome,
8945
9207
  resolveRequestId,
8946
9208
  seedPlatformSsoClient,
8947
9209
  ...require("@objectstack/core")